Skip to content

Phase 9: zero axe violations on login, dashboard and providers at 375-1440px, keyboard flows - #32

Merged
LMPrado-DZ23 merged 11 commits into
release/v3.8.54from
evolution/phase-9-a11y
Sep 18, 2026
Merged

LMPrado-DZ23 merged 11 commits into
release/v3.8.54from
evolution/phase-9-a11y

Conversation

@LMPrado-DZ23

Copy link
Copy Markdown
Owner

Phase 9 — accessibility and responsiveness

axe (wcag2a/2aa/21a/21aa) now finds 0 violations of any impact on /login, /dashboard and /dashboard/providers at 375, 768, 900, 1024, 1280 and 1440 px (before: /dashboard 2–4 serious, /dashboard/providers 2–3 serious per width).

Changes

  • useDialogFocus shared hook (Escape, initial focus, Tab trap that skips disabled controls, focus restore) used by Modal, ProviderTestSlideOver and the mobile drawer in DashboardLayout (now inert when closed, named role=dialog + aria-modal when open).
  • Checkbox labelled explicitly; Toggle switches described with aria-describedby.
  • SidebarSectionHeader: expand and pin are sibling buttons (was a div[role=button] wrapping a button — axe nested-interactive, not keyboard reachable); collapse control no longer inside aria-hidden; icon-only links named; glyphs aria-hidden.
  • Login: label tied to the password input, loading announced as status, failures as alert.
  • Breadcrumbs used nonexistent --text-primary/--text-secondary tokens (1.25:1 contrast in light mode); now real theme tokens. Inline body links underlined.
  • Light-theme contrast to WCAG AA: --color-text-muted #71717a → #65656e, --color-primary #e54d5e → #b83242 (dark theme keeps #e54d5e), muted text at reduced opacity uses the full token. The theme store no longer writes the default preset inline over the token.
  • E2E: viewports 375/768/900/1024/1280/1440; responsive spec now runs authenticated pages and reports offending elements; a11y.spec.ts requires zero critical/serious at 768–1440 (baselines lowered to 0, no rules disabled); new keyboard-a11y.spec.ts (keyboard sign-in with visible focus, skip link first, sidebar sections with Enter/Space).

Needs design sign-off (visible)

Darker light-theme primary and muted text; Kimi sponsor button #1783FF → #0b6ad4.

Verification (local)

Vitest 17 files / 91 tests; batch-b-final.test.ts 33/33; ESLint exit 0; complexity no new violations; Playwright responsive + keyboard specs pass against an isolated webpack dev server. Not covered: /dashboard/settings and other pages, dark-theme contrast.

🤖 Generated with Claude Code

zodyprado-web and others added 9 commits September 14, 2026 07:56
Extract Modal's Escape handler, initial focus, Tab trap and focus restore into
src/shared/hooks/useDialogFocus.ts so slide-over drawers can reuse the exact same
contract. The trap now skips disabled controls: a disabled last button refused
focus and let Tab escape the dialog.

Tests: tests/unit/shared/components/Modal.a11y.test.tsx (dialog name, aria-modal,
named close button, initial focus, Tab wrap skipping disabled, Escape);
tests/unit/ui/modal-focus-restoration.test.tsx unchanged and green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Checkbox generates an id when none is passed so the visible label keeps an
explicit htmlFor association. Toggle links its visible description through
aria-describedby when a label or ariaLabel already names the switch (no double
announcement when the description is the name).

Tests: tests/unit/shared/components/Checkbox.a11y.test.tsx and
tests/unit/shared/components/Toggle.a11y.test.tsx (role=switch, aria-checked,
Space/Enter, description, disabled).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
axe serious violations on /dashboard and /dashboard/providers:
- nested-interactive: section headers were div[role=button] wrapping the pin
  button and could not be reached from the keyboard. SidebarSectionHeader renders
  the expand control and the pin control as sibling buttons (aria-expanded,
  aria-pressed), same visual layout.
- aria-hidden-focus: the collapse button lived inside an aria-hidden container;
  only the decorative window dots are hidden now.
Collapsed (icon-only) nav links get aria-label, restart/shutdown get explicit
names, and Material Symbols glyphs are aria-hidden so ligature text is not read.

Tests: tests/unit/shared/components/SidebarSectionHeader.a11y.test.tsx and
tests/unit/shared/components/Sidebar.a11y.test.tsx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ProviderTestSlideOver: aria-modal, focus moves in, Tab is trapped, Escape closes
and focus returns to the opener (useDialogFocus); model/key selects are labelled;
the logs loading state is role=status and a load failure is role=alert; glyphs
are aria-hidden.

DashboardLayout mobile drawer: inert while closed (its links stayed in the tab
order and accessibility tree while translated off-canvas), role=dialog +
aria-modal + name while open, same focus contract, aria-hidden backdrop.

Tests: tests/unit/shared/components/ProviderTestSlideOver.a11y.test.tsx and
tests/unit/shared/components/DashboardLayout.drawer.a11y.test.tsx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The visible "Password" label is tied to the input (htmlFor/id), the initial
loading state is a polite status region, a failed sign-in is role=alert, and
decorative Material Symbols glyphs are aria-hidden. No behavior change.

Tests: tests/unit/ui/login-page-a11y.test.tsx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…links

Breadcrumbs read --text-primary/--text-secondary, which are not defined, so the
dark-theme fallbacks (#e0e0e0, diegosouzapw#888) rendered in light mode (axe color-contrast
1.25:1 on the current crumb). They now use --color-text-main/--color-text-muted
/--color-accent.

Home inline links in body copy are always underlined (axe link-in-text-block:
brand red vs muted grey text is 1.27:1, so color alone does not identify them).
HomePageClient.tsx line count unchanged (frozen file).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
After the structural fixes, axe color-contrast (serious) was the only remaining
violation on /dashboard and /dashboard/providers at every width. Fixed at the
token level where possible, measured against the backgrounds axe reported:

- --color-text-muted #71717a -> #65656e (light only): was 4.25:1 on bg-subtle,
  4.44:1 on the sidebar and 3.81:1 on control grays; now >= 4.55:1 on all.
- --color-primary #e54d5e -> #b83242 and hover #c93d4e -> #9f2a38 (light only):
  primary text on its /10 tint was 3.07:1 and white on primary 3.78:1; now 4.68:1
  and 5.88:1. .dark pins the original coral, which passes there (5.11:1) while
  the darker shade would not (3.29:1).
- Muted text with opacity modifiers (/40, /60, /70, opacity-70) cannot reach
  4.5:1 at any shade: sidebar section titles, item subtitles, group separators,
  no-results line, sponsor banner notes and provider category counts now use the
  full token. Decorative aria-hidden glyph opacities are unchanged.
- Sidebar restart/shutdown labels: amber-500/red-500 (1.96:1 / 3.5:1) ->
  amber-700/red-700 in light theme, 500 shades kept in dark.
- Kimi sponsor CTA: white on #1783FF was 3.67:1 -> #0b6ad4 (5.22:1). Partner
  accent colors elsewhere (card borders, KIMI_BRAND_COLOR) are untouched.
- Provider category pills expose aria-pressed like the media chips; the media
  chip glyph is aria-hidden.

Visual change: light-theme primary and muted text are visibly darker. Dark theme
is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…kens

themeStore wrote the preset color inline on <html> for every preset, including the
default "coral". An inline custom property beats both :root and .dark, so the
contrast-safe light-theme primary from globals.css never applied: axe still
measured #e54d5e (white on primary 3.78:1, primary text on its tint 3.07:1).

The default preset, and unknown ids that already fell back to it, now remove the
inline --color-primary/--color-primary-hover so the CSS tokens apply (deeper coral
in light, original coral in dark). Other presets and custom colors are user choices
and are still applied inline. COLOR_THEMES.coral is unchanged (Appearance swatch).

Tests: tests/unit/ui/theme-store-default-primary.test.tsx (6); existing
tests/unit/ui/providerIconKimiLogomark.test.tsx still green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rd flows

- tests/e2e/responsiveSpecs.ts: viewports 375/768/900/1024/1280/1440 (900 and 1024
  cover the band below the lg sidebar breakpoint, audit C-09). The overflow check
  returns the offending elements: the dashboard shell clips horizontal overflow, so
  body.scrollWidth alone could never fail there.
- tests/e2e/responsive.spec.ts: runs authenticated pages too (they were all
  skipped) and tolerates the /dashboard -> /home client redirect.
- tests/e2e/a11y.spec.ts: /login, /dashboard and /dashboard/providers must have zero
  critical/serious violations at every width from 768 to 1440; the total-count
  ratchet at 1280px is lowered to the measured values (1/4/3 -> 0/0/0).
  /dashboard/settings keeps 5 (not re-measured). No axe rules disabled. Types come
  from @axe-core/playwright via a type-only import (no casts).
- tests/e2e/keyboard-a11y.spec.ts (new): keyboard-only sign-in with a visible focus
  ring, skip link as the first tab stop, sidebar section headers toggled with
  Enter/Space. Dashboard tests open /home directly: /dashboard redirects there
  client-side and the redirect raced the shared auth helper's page.evaluate.
- tests/unit/batch-b-final.test.ts: the responsiveSpecs matrix assertions follow the
  new viewport list.

Measured locally against an isolated dev server (webpack, temp DATA_DIR/HOME,
port 20931) with @axe-core/playwright, wcag2a/2aa/21a/21aa: before 2-4 serious
violations per dashboard page (nested-interactive, aria-hidden-focus,
color-contrast, link-in-text-block); after 0 on all three pages at all six widths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 487bd862-c068-430f-98f0-d969337062a3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

zodyprado-web and others added 2 commits September 18, 2026 13:46
balance-e2e-shards.test.ts requires every e2e spec to have a weight in
config/quality/e2e-timings.json. keyboard-a11y.spec.ts (added in this branch)
had none, which failed the unit fast-path on PR #32. Weighted by its line count,
as the seed documents for new specs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in PR #35 (v3.8.54 cycle and the /api/metrics governance fix that
turned the base gate red, failing Fast Quality Gates and the governance unit
test on this PR).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LMPrado-DZ23
LMPrado-DZ23 merged commit 6edf22f into release/v3.8.54 Sep 18, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants